
package hk.ces.oxfam.patch.xml.factory;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.log4j.Logger;
import org.dom4j.Attribute;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.Node;
import org.dom4j.io.SAXReader;

/**
 *
 * @author ReutyCheng
 */
public class PredefinedDataDict
{
  private static class PredefinedDataDictHolder
  {
    private static PredefinedDataDict instance = new PredefinedDataDict();
  }
  
  private static final String NODE_PREDEFINDED_DATA_DICT = "PredefinedDataDict";
  private static final String NODE_PREDEFINDED_FIELD = "PredefinedField";
  private static final String NODE_EXCLUDE_TABLE = "ExcludeTable";
  private static final String NODE_EXTENDED_TABLE = "ExtendedTable";
  private static final String NODE_EXTENDED_FIELD = "ExtendedField";
  private static final String NODE_TABLE = "Table";
  private static final String NODE_FIELD = "Field";
  private static final String NODE_RELATED_TABLE = "RelatedTable";
  private static final String ATTR_EXCLUDE_TABLE_VALUE = "value";
  private static final String ATTR_EXTENDED_TABLE_NAME = "name";
  private static final String ATTR_EXTENDED_FIELD_NAME = "name";
  private static final String ATTR_EXTENDED_FIELD_DISC = "disc";
  private static final String ATTR_TABLE_NAME = "name";
  private static final String ATTR_TABLE_EXTENDS = "extends";
  private static final String ATTR_TABLE_DISC = "disc";
  private static final String ATTR_PREDEFINDED_FIELD_NAME = "name";
  private static final String ATTR_PREDEFINDED_FIELD_DISC = "disc";
  private static final String ATTR_FIELD_NAME = "name";
  private static final String ATTR_FIELD_DISC = "disc";
  private static final String ATTR_FIELD_EXCLUDE = "exclude";

  private final Document document;
  private final Map<String, String> predefinedFieldCache = new HashMap<String, String>();
  private final Map<String, Map<String, String>> extendedFieldsCache = new HashMap<String, Map<String, String>>();
  private final Set<String> excludeTableCache = new HashSet<String>();

  // Cache
  private String currentTable = null;
  private Map<String, String> currentFieldsDiscCache = new HashMap<String, String>();
  private Set<String> currentExcludeFieldsCache = new HashSet<String>();
  private List<String> currentRelatedTableCache = new ArrayList<String>();

  PredefinedDataDict()
  {
    SAXReader reader = new SAXReader();
    Document readDocument = null;
    try
    {
      readDocument = reader.read(this.getClass().getResourceAsStream("/data-dict-template.xml"));
    }
    catch (DocumentException ex)
    {
      Logger.getLogger(this.getClass()).error(ex.getMessage(), ex);
    }
    finally
    {
      this.document = readDocument;

      List predefinedFieldList = this.document.selectNodes("/" + NODE_PREDEFINDED_DATA_DICT + "/" + NODE_PREDEFINDED_FIELD);
      for (Object object : predefinedFieldList)
      {
        Element element = (Element) object;
        String[] predefinedFieldNames = element.attributeValue(ATTR_PREDEFINDED_FIELD_NAME).split(",");
        String predefinedFieldDisc = element.attributeValue(ATTR_PREDEFINDED_FIELD_DISC);

        for (String predefinedFieldName : predefinedFieldNames)
        {
          predefinedFieldCache.put(predefinedFieldName.toUpperCase(), predefinedFieldDisc);
        }
      }

      List extendedTablesList = this.document.selectNodes("/" + NODE_PREDEFINDED_DATA_DICT + "/" + NODE_EXTENDED_TABLE);
      for (Object extendedTable : extendedTablesList)
      {
        Element element = (Element) extendedTable;
        String extendedTableName = element.attributeValue(ATTR_EXTENDED_TABLE_NAME);

        List extendedFieldsList = element.selectNodes(NODE_EXTENDED_FIELD);
        Map<String, String> extendedFieldNames = new HashMap<String, String>(extendedFieldsList.size());
        for (Object extendedField : extendedFieldsList)
        {
          Element fieldElement = (Element) extendedField;
          
          String fieldName = fieldElement.attributeValue(ATTR_EXTENDED_FIELD_NAME);
          String fieldDisc = fieldElement.attributeValue(ATTR_EXTENDED_FIELD_DISC);
          if (fieldDisc != null)
          {
            extendedFieldNames.put(fieldName, fieldDisc);
          }
          else
          {
            extendedFieldNames.put(fieldName, predefinedFieldCache.get(fieldName));
          }
        }

        extendedFieldsCache.put(extendedTableName, extendedFieldNames);
      }

      List excludeTableList = this.document.selectNodes("/" + NODE_PREDEFINDED_DATA_DICT + "/" + NODE_EXCLUDE_TABLE);
      for (Object object : excludeTableList)
      {
        Element element = (Element) object;
        String excludeTableValue = element.attributeValue(ATTR_EXCLUDE_TABLE_VALUE);
        excludeTableCache.add(excludeTableValue.toUpperCase());
      }
    }
  }

  public boolean isTableExcluded(String tableName)
  {
    return excludeTableCache.contains(tableName.toUpperCase());
  }

  public boolean isTablePredefined(String tableName)
  {
    Node node = this.document.selectSingleNode(this.getTableXPath(tableName.toUpperCase()));
    return node != null;
  }

  public boolean isFieldPredefined(String tableName, String fieldName)
  {
    Node fieldNode = this.document.selectSingleNode(this.getFieldXPath(tableName.toUpperCase(), fieldName.toUpperCase()));
    if (fieldNode == null)
    {
      Node tableNode = this.document.selectSingleNode(this.getTableXPath(tableName.toUpperCase()));
      if (tableNode != null)
      {
        String extendedTables = ((Element) tableNode).attributeValue(ATTR_TABLE_EXTENDS);
        if (extendedTables != null)
        {
          for (String extendedTable : extendedTables.split("\\,"))
          {
            Map<String, String> extendedFields = extendedFieldsCache.get(extendedTable);
            if (extendedFields != null && extendedFields.containsKey(fieldName))
            {
              return true;
            }
          }
        }
      }
      return false;
    }
    
    return true;
  }

  public String getTableDisc(String tableName)
  {
    Node node = this.document.selectSingleNode(this.getTableXPath(tableName.toUpperCase()));
    if (node != null)
    {
      this.refreshCurrentTableCache(tableName.toUpperCase(), ((Element) node).attributeValue(ATTR_TABLE_EXTENDS));

      Attribute attribute = ((Element) node).attribute(ATTR_TABLE_DISC);
      if (attribute != null)
      {
        return attribute.getValue();
      }
      else
      {
        return null;
      }
    }
    else
    {
      return null;
    }
  }

  public String getFieldDisc(String tableName, String fieldName)
  {
    if (currentTable == null || !currentTable.equalsIgnoreCase(tableName))
    {
      getTableDisc(tableName);
    }
    
    String definedFieldDisc = currentFieldsDiscCache.get(fieldName.toUpperCase());
    return definedFieldDisc != null ? definedFieldDisc : predefinedFieldCache.get(fieldName.toUpperCase());
  }

  public boolean isFieldExclude(String tableName, String fieldName)
  {
    if (currentTable == null || !currentTable.equalsIgnoreCase(tableName))
    {
      getTableDisc(tableName);
    }
    
    return currentExcludeFieldsCache.contains(fieldName);
  }

  public List<String> getRelatedTables(String tableName)
  {
    if (currentTable == null || !currentTable.equalsIgnoreCase(tableName))
    {
      getTableDisc(tableName);
    }

    return currentRelatedTableCache;
  }

  private void refreshCurrentTableCache(String tableName, String extendsTables)
  {
    currentTable = tableName.toUpperCase();
    currentFieldsDiscCache.clear();
    currentExcludeFieldsCache.clear();
    currentRelatedTableCache.clear();

    if (extendsTables != null)
    {
      for (String extendsTable : extendsTables.split("\\,"))
      {
        Map<String, String> extendedFields = this.extendedFieldsCache.get(extendsTable);
        if (extendedFields != null)
        {
          currentFieldsDiscCache.putAll(extendedFields);
        }
      }
    }
    
    List currentFieldList = this.document.selectNodes(getFieldsXPath(currentTable));
    for (Object object : currentFieldList)
    {
      Element element = (Element) object;
      String fieldName = element.attributeValue(ATTR_FIELD_NAME);
      String fieldDisc = element.attributeValue(ATTR_FIELD_DISC);
      String fieldExclude = element.attributeValue(ATTR_FIELD_EXCLUDE);
      
      currentFieldsDiscCache.put(fieldName, fieldDisc);
      if (fieldExclude != null && Boolean.valueOf(fieldExclude))
      {
        currentExcludeFieldsCache.add(fieldName);
      }
    }

    List currentRelatedTableList = this.document.selectNodes(getRelatedTableXPath(currentTable));
    for (Object object : currentRelatedTableList)
    {
      Element element = (Element) object;
      String relatedTable = element.getTextTrim();
      currentRelatedTableCache.add(relatedTable);
    }
  }

  private String getTableXPath(String tableName)
  {
    return "/" + NODE_PREDEFINDED_DATA_DICT + "/" + NODE_TABLE + "[@" + ATTR_TABLE_NAME + "='" + tableName + "']";
  }

  private String getRelatedTableXPath(String tableName)
  {
    return "/" + NODE_PREDEFINDED_DATA_DICT + "/" + NODE_TABLE + "[@" + ATTR_TABLE_NAME + "='" + tableName + "']/" + NODE_RELATED_TABLE;
  }

  private String getFieldsXPath(String tableName)
  {
    return "/" + NODE_PREDEFINDED_DATA_DICT + "/" + NODE_TABLE + "[@" + ATTR_TABLE_NAME + "='" + tableName + "']/" + NODE_FIELD;
  }

  private String getFieldXPath(String tableName, String fieldName)
  {
    return "/" + NODE_PREDEFINDED_DATA_DICT + "/" + NODE_TABLE + "[@" + ATTR_TABLE_NAME + "='" + tableName + "']/" + NODE_FIELD + "[@" + ATTR_FIELD_NAME + "='" + fieldName + "']";
  }

  public static PredefinedDataDict get()
  {
    return PredefinedDataDictHolder.instance;
  }
}
